"""
PaperTiger A Paper Prototyping Helper Program

PaperTiger process image and draw window from the image.
All spaces roughly covered by lines (with certain area) becomes component
and callbacks can be assigned to them.

==== Simple Sample =====
import papertiger
def test(event): print("Override test")

pwindows = papertiger.analyzeImage("filename.png")
assert len(pwindows) > 0, "Invalid image file"
pwnd = pwindows[0]
pwnd.getSubComponent("0").leftdown = test

papertiger.loadWindow(pwnd)
====

Author: tomoiida
(C) 2012 Tomohisa Iida

"""
import Image as pil
import ImageDraw
import imageproc
import sys

from Tkinter import *
from ttk import *
import ImageTk

def rescaleBox(box, width, height):
  return imageproc.Box([v*scale for v,scale in zip(box.coords, (width,height,width,height))])

class PaperComponent(imageproc.Box):
  """
  Basic Component of PaperTiger

  Spaces roughly covered by a line with certai area becomes PaperComponent.
  PaperComponents have a tree-structure and have children.
  You can add callbacks to components, switch image or what ever you want.

  Position of a component is stored in coord tuple (left, top, right bottom), and 
  each length is ratio to its window size.

  """
  def __init__(self, parent, r_box, img, bimage=None):
    def create_child_component(box):
      w,h = bimage.size
      rx1, ry1, rx2, ry2 = self.coords
      rw,rh = rx2 - rx1, ry2 - ry1
      new_image = imageproc.cut_img(img, box)
      draw.rectangle(box.coords, fill="white")
      new_bimage = imageproc.cut_img(bimage, box)
      x1,y1,x2,y2 = box.coords
      return PaperComponent(self, imageproc.Box(
        rx1 + rw * x1 / w,
        ry1 + rh * y1 / h,
        rx1 + rw * x2 / w,
        ry1 + rh * y2 / h
      ), new_image, new_bimage) 

    self.coords = r_box.coords
    self.image = img
    self.parent = parent
    draw = ImageDraw.Draw(self.image)
    bimage = bimage or imageproc.binarizeImage(img)
    boxes = imageproc.separate_boxes(imageproc.remove_outframe(bimage))
    self.children = [create_child_component(b) for b in boxes]

  def scanComponent(self, proc, tpl = ()):
    proc(self, tpl)
    for i,child in enumerate(self.children):
      child.scanComponent(proc, tpl + (i,))

  def findComponentAt(self, x, y):
    if self.is_inside((x,y)):
      for child in self.children:
        ret = child.findComponentAt(x,y)
        if ret != None: return ret
      return self
    return None

  def leftdown(self, event):
    """ override point of left-down callback"""
    return self.parent.leftdown(event)

  def rightdown(self, event):
    """ override point of right-down callback"""
    return self.parent.rightdown(event)

  def leftup(self, event):
    """ override point of left-up callback"""
    return self.parent.leftup(event)

  def rightup(self, event):
    """ override point of right-up callback"""
    return self.parent.rightup(event)

  def leftdbldown(self, event):
    """ override point of left-double-click callback"""
    return self.parent.leftdbldown(event)

  def rightdbldown(self, event):
    """ override point of right-double-click callback"""
    return self.parent.rightdbldown(event)

  def _sub_component(self, l):
    if len(l)==0: return self
    i = l.pop()
    return self.children[i]._sub_component(l)

  def getSubComponent(self, s):
    """search component under this component by alias or tuple
   
    Alias is shown as "id1:2" works as (1,2) and which pickes
    component.children[1].children[2] like so.
    alias is shown when p button is pressed or paperwindow.preview().
    """
    if isinstance(s, str):
      if s.startswith("id"): s = s[2:]
      if s == "": return self
      l = [int(x) for x in s.split(":")]
    elif isinstance(s, list):
      l = s
    else:
      l = list(s)
    l.reverse()
    return self._sub_component(l)

  def replaceImage(img):
    """ replace component with image. Sub components are reconstructed"""
    new_component = PaperComponent(parent, self, img)
    self.replaceComponent(new_component)

  def replaceComponent(new_component):
    """ replace component with existing component."""
    def resize_subcomponents(component, tpl):
      l,t,r,b = component.coords
      ow,oy = self.size()
      nl,nt,nr,nb = newbox.coords
      nw,nh = nr - nl, nb - nt

      component.coords = (nl+nw*l/ow, nt+nh*t/oh, nl+nw*r/ow, nt+nh*b/oh)

    if new_component.coords != self.coords:
      resize_subcomponents(new_component, self.size(), self.coords)

    parent = self.parent
    self.parent = None
    i = parent.children.index(self)
    parent.children[i] = new_component
    new_component.parent = parent


class PaperWindow(PaperComponent):
  """ Window class"""
  def __init__(self, im):
    self.rootimg = im.copy()
    PaperComponent.__init__(self, None,
        imageproc.Box(0.0, 0.0, 1.0, 1.0), im, imageproc.binarizeImage(im))


  def preview(self):
    """ Outline Components and show their alias(like "idXX:YY:ZZ")."""
    def proc(components, idtpl):
      W,H = self.rootimg.size
      for i,component in enumerate(components):
        T = idtpl + (i,)
        realbox = rescaleBox(component, W, H)
        draw.rectangle(realbox.coords, outline = (255, 200, 200))
        s = "id" + str(":".join([str(t) for t in T]))
        w,h = draw.textsize(s)
        draw.text(((realbox.width() - w)/2 + realbox.left(), (realbox.height() - h)/2 + realbox.top()), s, fill="red")
        proc(component.children, t)

    img = self.rootimg.copy()
    draw = ImageDraw.Draw(img)
    proc(self.children, ())
    img.show()

  def drag(self, event):
    """ override point of dragging. """
    pass
    

def analyzeImage(img):
  """Analyze image and returns PaperWindow List generated from the image."""
  if isinstance(img, str):
    img = pil.open(img)
  return [PaperWindow(img) for img in imageproc.separate_windows(img)]

class TkWindow(Tk):
  """TkInter window class.
  
  """
  def __init__(self, *args, **kwargs):
    Tk.__init__(self, *args, **kwargs)
    self.overrideredirect(1)
    self.column = Frame(self)
    self.column.pack(side=RIGHT, anchor="nw")
    self.close_button = Button(self.column, text="x", command=self.close, width=2)
    self.close_button.pack(side=TOP)
    self.prev_button = Button(self.column, text="p", command=self.preview, width=2)
    self.prev_button.pack(side=TOP)
    #spacer = Frame(self)
    #spacer.pack(fill=Y,expand=True)
    self.canvas = Canvas(self,background='#FFAAAA')
    self.canvas.pack(fill = BOTH, side=RIGHT, expand=True)
    self.dragging = False
    self.resizing = False
    
    #img = ImageTk.PhotoImage(pil.open("pt.png"))
    #self.canvas.create_image(0,0,anchor=CENTER, image=img)
    self.bind("<B1-Motion>", self._drag)
    self.bind("<Motion>", self._move)
    self.bind("<Leave>", self._leave)
    self.bind("<Button-1>", self._root_mouse_button_event)
    self.canvas.bind("<Button-1>", self._mouse_button_event)
    self.canvas.bind("<ButtonRelease-1>", self._mouse_button_event)
    self.canvas.bind("<Double-Button-1>", self._mouse_dbl_button_event)
    self.canvas.bind("<Button-2>", self._mouse_button_event)
    self.canvas.bind("<ButtonRelease-2>", self._mouse_button_event)
    self.canvas.bind("<Double-Button-2>", self._mouse_dbl_button_event)
    self.canvas.bind("<Configure>", self._onchange)

  def _onchange(self, event):
    self.drawWindow()

  def close(self):
    """ close root window. """
    self.quit()

  def event_component(self, event):
    def remove_highlight():
      self.canvas.delete(rect_id)

    canvas_w,canvas_h = self.canvas.winfo_width(),self.canvas.winfo_height()
    x,y = 1.0 * event.x / canvas_w, 1.0 * event.y / canvas_h
    component = self.current_window.findComponentAt(x,y)
    if component == None: return
    box = rescaleBox(component, canvas_w, canvas_h)
    rect_id = self.canvas.create_rectangle(box.left(), box.top(), box.right(), box.bottom(), width="3", outline="#FAA")
    self.canvas.after(100, remove_highlight)
    return component
    
  def _leave(self, event):
    print("LEAVING")
    self.config(cursor="")

  def _move(self, event):
    if event.y < 10:
      self.config(cursor="fleur")
    elif self.winfo_width() - event.x < 10 and self.winfo_height() - event.y < 10:
      self.config(cursor="bottom_right_corner")
    else:
      self.config(cursor="")
    component = self.event_component(event) or self.current_window


  def _drag(self, event):
    if self.dragging:
      self.geometry("%dx%d+%d+%d"%(
        self.winfo_width(),self.winfo_height(),
        self.start_offset[0] + event.x_root,
        self.start_offset[1] + event.y_root))
      return
    elif self.resizing:
      self.geometry("%dx%d+%d+%d"%(
        max(self.start_offset[0] + event.x_root, 100),
        max(self.start_offset[1] + event.y_root, 100),
        self.winfo_x(),
        self.winfo_y()))
      return

    self.current_window.drag(event)
  
  def _root_mouse_button_event(self, event):
    event_type = {
      (1,4): "leftdown",
      (2,4): "rightdown",
      (1,5): "leftup",
      (2,5): "rightup"
    }[(event.num, int(event.type))]

    if event_type == "leftdown" and event.y < 10:
      self.start_offset = self.winfo_x() - event.x_root, self.winfo_y() - event.y_root
      self.dragging = True
      self.resizing = False
      return "break"
    elif event_type == "leftdown" and self.winfo_width() - event.x < 10 and self.winfo_height() - event.y < 10:
      self.start_offset = self.winfo_width() - event.x_root, self.winfo_height() - event.y_root
      self.resizing = True
      self.dragging = False
      return "break"
    elif event_type == "leftup":
      self.dragging = False
      self.resizing = False
      self.config(cursor="")
      return "break"

  def _mouse_button_event(self, event):
    component = self.event_component(event) or self.current_window
    {
      (1,4): component.leftdown,
      (2,4): component.rightdown,
      (1,5): component.leftup,
      (2,5): component.rightup
    }[(event.num, int(event.type))](event)

  def _mouse_dbl_button_event(self, event):
    component = self.event_component(event) or self.current_window
    {
      (1,4): component.leftdown,
      (2,4): component.rightdown,
      (1,5): component.leftup,
      (2,5): component.rightup
    }[(event.num, int(event.type))](event)

  def drawWindow(self, paperwindow = None):
    """ Switch paperwindow and redraw all items. """
    def show_component(component, tpl):
      l,t,r,b = component.coords
      box = imageproc.Box(int(W*l), int(H*t), int(W*r), int(H*b))
      component.tkimg = ImageTk.PhotoImage(component.image.resize(box.size()))
      #self.canvas.create_rectangle(box.left(), box.top(), box.right(), box.bottom(), width="3", outline="#FAA")
      tkobj = self.canvas.create_image(box.left(), box.top(),anchor="nw", image=component.tkimg)
      component.tkobj = tkobj

    self.canvas.delete(ALL) ## TODO replace partially redraw for better performance.

    if paperwindow != None: self.current_window = paperwindow
    paperwindow = self.current_window 

    paperwindow.parent = self
    W = self.canvas.winfo_width()
    H = self.canvas.winfo_height()
      
    self.current_window = paperwindow
    paperwindow.scanComponent(show_component)
    #self.canvas.create_line(0,0,100,100)
    #self.canvas.create_line(0,100,100,0)

  def preview(self):
    """show components and its alias"""
    effects = []
    def show_component_name(component, tpl):
      l,t,r,b = component.coords
      box = imageproc.Box(int(W*l), int(H*t), int(W*r), int(H*b))
      effects.append(self.canvas.create_rectangle(box.left(), box.top(), box.right(), box.bottom(), width="3", outline="#FAA"))
      text = "id" + ":".join([str(t) for t in tpl])
      x,y = box.center()
      textsize = min(box.height()/4, 20)
      effects.append(self.canvas.create_text(x, y, text=text, fill="#FAA", anchor="n", font=("FixedSys",textsize)))

    def del_effect():
      for e in effects: self.canvas.delete(e)
    W,H = self.canvas.winfo_width(), self.canvas.winfo_height()
    self.current_window.scanComponent(show_component_name)
    self.canvas.after(500, del_effect)

  def leftdown(self, event): print("Left pressed. Override 'leftdown'")
  def leftup(self, event): pass
  def rightdown(self, event): print("Right pressed. Override 'rightdown'")
  def rightup(self, event): pass
  def leftdbldown(self, event): print("Left Double Clicked. Override 'leftdbldown'")
  def rightdbldown(self, event): print("Right Double Clicked. Override 'rightdbldown'")

def loadWindowAndStart(pwnd, resize=True):
  """ Load PaperWindow to the root window and start application. """
  global root
  root = TkWindow()
  root.current_window = pwnd
  if resize:
    w,h = pwnd.rootimg.size
    root.geometry("%dx%d+%d+%d"%(w+60,h,100,200))
  root.mainloop()

if __name__=='__main__':
  import sys
  windows = []
  if len(sys.argv) > 1:
    for filename in sys.argv[1:]:
      windows += analyzeImage(sys.argv[1])
  else:
    windows = analyzeImage("pt.png")
  print("%d windows"%len(windows))
  #windows[0].preview()

  if len(windows) > 0:
    loadWindowAndStart(windows[0])
  else:
    print("ERROR: NO VALID WINDOW")

